I first started writing code to produce TIFF files fifteen or twenty years ago. What I wanted to do was create a lens resolution test chart. So it could print at convenient sizes like 8"x10" or 8½"x11", I made it 7" x 9½". At 300 DPI this is a size of 2100 x 2850 pixels. Rather than have an image array of this size, because memory was still relatively expensive back then, I made the program generate one line of the image at a time, which was then repeated some number of times before changing it. This required only a line buffer of 2100 bytes.
Eventually I wanted to both read in and write out existing TIFF scans of my slides and negatives, so I've developed quite a lot of experience figuring out how to do this. The TIFF v.6 PDF file is pretty good as such documents go, so I recommend starting at the beginning and reading down through it section by section as it introduces how to do increasingly more sophisticated image types, up to greyscale (8-bit, "B&W") pictures. At the end of each section it shows examples of the dozen or fifteen TIFF tags required as a minimum to produce each kind of file (FAX, palette, greyscale, etc.).
Reading a TIFF file is much more complicated than writing one because a program needs to be able to recognize and handle all kinds of possible TIFF tags, not just the minimum required, whereas writing out a legit file needs only the minimum, and maybe a couple of common but optional ones. So I'll concentrate on the writing of TIFF image files first.
The program I've been using is FreeBASIC. To use the code I present below you should first go to their website and download and then install their program. You want the "IDE" version, this standing for Integrated Development Environment. All this means is when you run the program it will open up its own text editor. Once you type in and/or read in some program code you can both compile it and then run the stand-alone .exe programs it creates from within the FreeBASIC IDE program. You can also easily access the (immense) Help files. The editor recognizes FreeBASIC keywords and will show them in a distinct color and/or highlight them, depending on the Preferences settings, which are also accessible under the View menu.
The program both reads and writes your code in plain text files just like Notepad, even though they have a .bas extension rather than a .txt one. After starting the program I recommend first hitting the F4 key, which will open up a "results" window below the editor; this is where any errors will be shown when you compile your code. (This window can also be opened up under the "View" menu at top left.) If you get an error about a "missing language file" when starting the program this is normal for me and seems to have no effect; click "Ok" to get by it. The other thing you'll want to do when compiling code is notice that in the very lower left corner of the program's window border it will say "Compiling..." and then "Compilation Complete" when it's done. If you didn't see this you might not know anything was happening during compilation.
But before getting to TIFF files let me switch to how to do greyscale BMP files, because they're actually much easier. Below you'll find the code for a subroutine which writes out a header for a BMP file. As well I then present the complete program code for an example showing its use to then write out the image data.
The subroutine takes just four numbers: the file number for the file being written out (which would have been assigned when you opened the file for output, and is usually a small number), the image width, the image height, and a number of padding bytes to add at the end of each row of the image.
This last number requires a little explanation as it's one of the idiosyncrasies of the BMP format. Each row written out has to have a number of bytes or pixels which is evenly divisible by four. So if your image width is 200 pixels, the number of padding bytes needed is zero. If your width was 201, you'd then need 3 padding bytes. (Look into the MOD function in the Help section for how you could program this so you wouldn't have to do it by hand.)
Once the header is written out one then can write out the image data to the file. This brings one to the next idiosyncrasy of the BMP format, namely that the image data has to be written from bottom to top. Every other format I know of goes from top to bottom. You'll see in the example program how an image array index counter which counts down from the image height to zero accomplishes this.
The BMP_Header subroutine is presented as a single block of text below. You'll want to highlight it, and then copy and paste it into an empty Notepad window. Save it as BMP_Header.bas so that you can later copy and paste into any program where you want it. (One could also use the #INCLUDE pre-processor directive, but that's a more advanced topic.)
The first thing to notice about this code is that everything following an apostrophe on a line is a comment, which is ignored by the compiler.
Second, FreeBASIC is what's called a strongly typed language, which means every variable has to be defined, or dimensioned (by the DIM command), before it can be used. These definitions are almost always found at the very beginning of the code, before anything is actually done, with each variable of the type being specified separated by a comma.
There are three principle types of variables: integer, floating point (decimal numbers), and strings. The latter are strings of characters, like text, and are found within a pair of double quotes. Decimal numbers are of two sub-types: SINGLE and DOUBLE, depending on their precision; SINGLEs will show six digits after the decimal point and are sufficient for most, but not all, calculations. Integer variables are the most varied in sub-type, as they can be of three different lengths and, thus, of capacity: byte, word (2 bytes), or long (4 bytes). In addition, they can be either signed (the default) or unsigned. FreeBASIC has several shorthand ways of referring to these; for example, a USHORT variable is an unsigned short (2-byte or word) integer, which can range from zero up to 65,535 (= 2^16 - 1). These are used a lot when simply counting things.
Finally, because of the large variety of different variable types there are a number of built-in functions which convert between them. For example, CSNG() converts whatever is inside the parentheses (like an integer of some type) into a floating-point SINGLE, while CINT() converts into an integer, rounding the decimal part up or down as appropriate. The CAST(,) operator is the most general conversion function, if there's not a shorthand version. See the Help documentation. FreeBASIC is sophisticated enough that when different variable types are mixed in a calculation, like multiplying a decimal number by an integer, the complier will automatically "promote" all the variables to the "highest" type (here a decimal number), but I like to make it explicit by using the conversion functions. Combining different variable types in a calculation can be a hidden source of problems, so if code is doing seemingly mysterious things this is something to look at.
SUB BMP_Header(ByVal filenum AS INTEGER, ByVal ImgW AS USHORT,_ ByVal ImgH AS USHORT, ByVal Pad AS UBYTE) ' ' Writes out to an already opened file a BitMap Header for an ImgW x ImgH ' 8-bit paletted greyscale image. Pad is the number of padding bytes (0-3) ' that will need to be written at the end of each line to fit the word ' alignment convention. ' DIM AS USHORT I ' ' 14 byte Bitmap file header (BITMAPFILEHEADER): PUT #filenum,, "B" PUT #filenum,, "M" PUT #filenum,, MKL(CAST(ULONG, ImgH*(ImgW+Pad) + 794)) ' total file size PUT #filenum,, MKL(0) ' 4 bytes reserved PUT #filenum,, MKL(794) ' offset to start of bitmap image data ' = total length of header ' 12 byte DIB header / bitmap information header (BITMAPCOREHEADER): PUT #filenum,, MKL(12) PUT #filenum,, MKSHORT(ImgW) PUT #filenum,, MKSHORT(ImgH) PUT #filenum,, MKSHORT(1) PUT #filenum,, MKSHORT(8) ' ' 768 byte color table (RGB24 format), 3 bytes per entry: FOR I=0 TO 255 PUT #filenum,, CHR(I,I,I) NEXT I ' EXIT SUB END SUB
Well, the nice thing about subroutines is that once they're written you don't have to worry about their internal details any more. You just use them. But in case someone's interested, they'll notice the use of the PUT # command, which outputs data to the file number or variable after the hashtag (#). In this instance, two additional built-in, special conversion functions are used: MKL (MAKE LONG) and MKSHORT (MAKE SHORT). Both these take a number or variable of the corresponding type and convert it into its binary representation as a string of bits, which is what's required here by the format specifications.
And, even though I've referred to the BMP file being created here as a greyscale image, technically it's a paletted, or color tabled (as it's called here) file. This is what's written out last in the header subroutine, and consists of 256 triplets of RGB values (three times one byte each) that the pixel values correspond to. One could get creative here and make various pixel values correspond to all sorts of colors, but the FOR ... NEXT loop construct in this case simply writes out triplets of R=G=B=pixel value greys from 0 to 255, producing the equivalent of a greyscale color table. If you were to fool around with this you'd want to be sure you wrote out 3x256=768 bytes. The CHR (character) function is needed to make the numbers output to the file into the equivalent of characters, again because the format requires this. Once the NEXT I statement causes I to exceed 255, the subroutine hits the exit statement and program execution returns to the main program statement following the call of the subroutine.
Without further ado, here's the program code showing how to use the subroutine, generate, and output a BMP graphic:
' ' BMP-DEMO.bas - Demonstrates the use of the BMP header subroutine ' BMP_Header by making a simple BMP graphic with ' the size W x H, both of which have to be evenly ' divisible by four, and in the ratio W/H = 3/2. ' ' The factor in line #45 (1.80277) controls the ' image contrast, here at a maximum; it can only ' be lowered, which will not affect the whites ' but make the blacks less black. ' ' DIM AS USHORT W = 600, H = 400, W1 = W - 1, H1 = H - 1 DIM AS UBYTE IMG(W1,H1) ' the image array, starts w/(0,0) DIM AS USHORT I, J, W2 = W/2, H2 = H/2, W21 = W2-1, H21 = H2 -1 DIM AS SINGLE X, Y, PX, PY, DX, DY, D DIM AS STRING DS ' DECLARE SUB BMP_Header(ByVal filenum AS INTEGER, ByVal ImgW AS USHORT,_ ByVal ImgH AS USHORT, ByVal Pad AS UBYTE) ' BMP file header writer ' note: the underscore at end of 1st line is a "continuation" symbol ' for code that is too long to fit on a single line. ' ' start of program: CLS ' clears the screen COLOR 14 ' sets text color to Yellow PRINT " BMP writing demo program, by Chris Wetherill" PRINT " --------------------------------------------" COLOR 15 ' makes all text White PRINT ' just a blank line INPUT " >> Hitto start: ", DS OPEN "Test-graphic.bmp" FOR OUTPUT AS #2 BMP_Header(2, W, H, 0) ' the call to the header subroutine ' ' make image graphic in IMG array: FOR J=0 TO H21 ' the top half of the graphic Y = CSNG(J) FOR I=0 TO W21 ' left half X = CSNG(I) PX = (9*X + 6*Y)/13 ' PX,PY is point on diagonal PY = 2 * PX / 3 ' perpindicular to (I,J) DX = PX - X ' delta x DY = PY - Y ' delta y D = SQR(DX*DX + DY*DY) ' distance from (I,J) to (PX,PY) IMG(I,J) = CUBYTE( 255 * (1 - 1.80277*D/W21) ) ' <<<<<< NEXT I FOR I=W2 TO W1 ' right half is a mirror image of the left IMG(I,J) = IMG(W1-I,J) NEXT I NEXT J ' FOR J=H2 TO H1 ' the bottom half of the graphic is a mirror image of top FOR I=0 TO W1 IMG(I,J) = IMG(I,H1-J) NEXT I NEXT J ' ' write out the image array: J = H DO J -= 1 ' decrements J by 1 (i.e., J = J - 1) FOR I=0 TO W1 ' wrtie out one line of the image PUT #2,, CHR(IMG(I,J)) NEXT I LOOP UNTIL J=0 CLOSE #2 ' don't forget to properly close a file that has been opened! ' INPUT " >> Hit to end/exit: ", DS STOP END ' ' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' ' BMP_Header subroutine goes in place of this line. '
The first thing to notice in the main program is how it has to be made explicitly aware of the subroutine, by using the DECLARE statement. I usually do that after defining all the variables. The other thing you'll see is how variables can be initialized with specific values, or even arithmetic expressions (after the needed variables used have been defined); otherwise they all default to a value of zero.
I've used code like this to generate many of the graphs you see here and around the site. After setting every pixel in the IMG(,) array to white (255), it's like a blank sheet of paper. It's a simple matter to draw x and y axes 50 or 75 pixels in from the edge. Then, just like with graph paper, you need to give some thought to the range in x values and the range in y values that apply for your particular problem, which will give you horizontal and vertical scaling factors, along with their corresponding offsets from zero. Remember that while regular graph paper will usually have y increasing upwards, for computer graphics y increases downwards. A good check that you've got everything correct is to then put tick marks on the axes. (This will likely use the STEP parameter with a FOR ... NEXT loop, this defaulting to 1 in its absence, so that the NEXT I (or whatever) will increase I by something like 50 or 100 instead of only 1.) After that, the (x,y) points that are calculated by the code can be converted into (I,J) array index values. Setting that pixel to zero then plots a black point. If you want something bigger, like a "+" sign, simply set the four adjacent pixels black also.
I do use a spreadsheet program (OpenOffice), but there are some calculations which are too difficult, complicated, or impossible to do that way. Plus, the graphs are pretty crude. Hence the need to be able to write custom code to do the calculations and make the graphs. But instead of trying to read spreadsheet files directly into my programs, I take the column(s), say, of intermediate values, highlight them, and then copy and paste them into Notepad, saving them as a text file. This is easily read into a FreeBASIC program, using the OPEN FOR INPUT command to start.
The only thing I haven't tackled yet in my code is the labelling of my graphs. In the directory where the FreeBASIC program installs you'll find lots of example programs. In the "examples\graphics\FreeType" directory there's a program, char.bas, which is about fonts and rendering text into a bitmap, but I haven't spent the time yet to integrate it into my programs. So I load my resulting BMP files into an image editing program and do the labelling there. This is necessary in any event, since the BMPs are uncompressed and thus take up a lot of file space. Saving them back out as compressed TIFFs or PNGs reduces their size by a lot, as there's usually much blank (white) space.
Ok, on to TIFFs... At the highest conceptual level a TIFF has just three parts: an 8-byte "header" identifying it as a TIFF, a variable length Image File Directory (IFD) where the relevant meta information about the image is found, and the image data itself.
The 8-byte header is composed of three fields, the first two being 2 bytes long and the last 4 bytes.
The first 2-byte field is either "II" or "MM" (without the quotes) and specifies the byte order employed by the file. "II" stands for the Intel byte order convention, which is least-significant byte first (LSB), sometimes called little-endian. "MM" stands for Motorola byte order, which is most-significant byte first (MSB), or big-endian. The way we usually write numbers in the west, left to right, is MSB, though there's no intrinsic advantage to doing it one way rather than the other, which is why there's two possibilities and both are available in TIFF.
Obviously, a full-service TIFF reading program needs to be able to handle either byte order, whereas a reading/writing program on a given machine really only needs to know which byte order convention it uses. If you don't know which your machine is, the simplest way to find out is just to guess, which will be right 50% of the time. If things don't work, switch to the other byte order. If you have an AMD processor (CPU), mine is II. The free program AsTiffTagViewer will tell you the byte order for a given TIFF file, as well as a bunch of other things about the file.
The second 2-byte field is just the number 42, which is the ASCII character code for an asterisk. This serves as a check on the byte order specified in the first field, since in II format it will be "* ", whereas in MM format it'll be " *" (again, without the quotes).
The third field in the header is an unsigned 4-byte (long) integer which is a "pointer" (or offset within the file) to where the (first) IFD can be found. Almost everything in a TIFF file works like this, with pointers to where things are in the file. If the IFD immediately follows the header this will just be the number 8.
Why wouldn't the IFD always just follow the header? Because of image data compression. When a picture is compressed, you don't know until you've done the compression how many bytes it'll be turned into. But this number is needed for the relevant field in the IFD, so the reading program knows how many bytes to read and then de-compress. So, when making compressed image files, it makes most sense to do the compression and write the results out to the output file, counting bytes as one goes. After completion one can then write out the IFD, which will thus be after the image data it describes in the file. One then has to go back up and put the proper number in the third field of the header to point to where the IFD is.
Fortunately this little complication doesn't matter to me, because my code only reads and writes uncompressed data. So the IFD follows the header and thus has an offset of 8.
The IFD itself has a simple structure. It consists, first, of a 2-byte unsigned integer field telling how many directory entries there are which follow (N). This will usually be a number of at least a dozen and maybe more like fifteen or sixteen, but it can be somewhat larger if one chooses to use some entries which are strictly optional. There are several dozen TIFF "tags", as they're called, though many are only needed for certain kinds of files.
Each directory entry itself is a 12-byte structure broken up into four parts: first, two 2-byte fields, followed by two 4-byte fields. All are unsigned integers. More on these in a second.
The last thing in an IFD is a 4-byte unsigned integer pointing to the/any next IFD. You may have noticed the word "first" in parentheses a few paragraphs back... A TIFF file can contain multiple pictures. (The only real limit is how big a file you want to deal with.) These are sometimes called multi-page TIFFs. The way this works is the IFDs are part of what's called a linked list, with the first one pointing to the second one, which points to the third one, and so on, with an IFD for each picture. The last one in the chain terminates the list by having the pointer in this next IFD field be zero. So if your file only has a single image in it, like all my code creates, this final field in the IFD will just be zero. The only restriction on these IFD offsets (or pointers) is that they have to be an even number.
Okay, back to the all-important IFD entries themselves. As I said, there are four fields. The first, a 2-byte (short) unsigned integer is the all important TIFF tag code, telling what this entry in the IFD has in it in the other three fields. For example, the number 258 here is the code for the number of bits per sample (bits per pixel), typically 8 for greyscale (B&W) or 24 for RGB color, this number being found in the last of the four fields of the directory entry.
This is the place for a lengthy digression on what are called
pre-processor directives (or commands). First introduced
in the c programming language in the 1970s, and exactly the same
in FreeBASIC, these are usually found at the top of a program,
before any variables have even been defined. All start with a
hashtag (#) at the beginning of the line and command, and tell
FreeBASIC what to do to the code itself before compiling
it (hence the "pre-processor" name). For example, the line
#DEFINE xyz 123
tells FreeBASIC to look for every instance of the string "xyz"
in the code and replace it with the string "123" before then
trying to compile the code. Note that the strings are delimited
by spaces not by the usual parentheses.
If the utility of this is not immediately apparent, consider
trying to remember all the TIFF tag codes and what they refer
to. Instead, we'd use a line at the top of our code like
#DEFINE TIFF_BitsPerSample 258
so that our program would use TIFF_BitsPerSample in the relevant
places, so the code was readable, but would then have this string
in the code replaced before compilation by the #DEFINE command
with the appropriate number. Note that TIFF_BitsPerSample is
not the name of a variable since by the time the compiler
sees the code it has been changed into the number 258 by the
substitution.
An obvious problem would be too many #DEFINE lines at the top
of a program, cluttering it up. My various programs reading TIFFs
have encountered some thirty different TIFF tag codes. Wouldn't
it be better to simply put these all into an external file
composed of thirty #DEFINE statements and then just read that
in at the top of the program? We can do that! With the #INCLUDE
statement. This has the filename in parentheses after a space.
An example would look like this:
#INCLUDE "TIFF_tags.inc"
In spite of the ".inc" extension, this is really just a plain
ASCII text file and can have any extension; several commonly
used ones are ".i" (short for "include") or ".h" ("header").
The FreeBASIC pre-processor simply copies what's in the file
into the program code at the location where the #INCLUDE
statement is found, and then picks up at the beginning of
the copied text, so some people use these for subroutines,
functions, and the like, since then it's easy to use the same
code in many different programs without having to copy it.
Just include the relevant file. In other words, the included
file doesn't necessarily have to contain any pre-processor
directives at all, though that's one of its common uses.
Now of course there are "official" TIFF sites out there where you can get an include file with all zillion TIFF tag codes in it, only a small number of which you might ever cross paths with, so I use my own, adding to the list as I encounter new codes. My programs issue a soft error if they read an unknown code, printing out the entire entry; it's then easy to go out on the I-Net and find out what the tag code refers to.
With that digression over, back to the TIFF IFD directory entries... So the first field is the TIFF tag code telling you what the entry is all about. The second 2-byte unsigned integer field is a code specifying what the data-type in the data field is. There are a bunch of these listed in the TIFF .pdf document, but the ones I encounter and use range only from 1 to 5:
That last one requires some explanation. This is TIFF's way of storing decimal (or floating point) numbers. At the time TIFF was first developed there was no agreed upon way of representing such numbers internally in computers. It was "machine dependent". So, a RATIONAL is represented as the ratio (or fraction) of two 4-byte long integers; the first represents the numerator of the fraction, the second the denominator. Because long integers range up to rather big numbers this is a decent machine independent way of storing decimal numbers.
An example might be the TIFF_XResolution and TIFF_YResolution entries. These specify the pixel spacing, or the number of pixels per inch on a digital camera's sensor. (There are two to allow for the possibility of non-square pixels.) For camera The number in the specifications section of the instruction booklet for the camera works out to 4457.29 pixels per inch, truncated to two places after the decimal. (From the TIFF_ResolutionUnit field which follows this could be either in pixels per inch or per centimeter.) The simplest way to represent this as a RATIONAL would be to use 445729 for the numerator and 100 for the denominator. There is one other complication with RATIONALs which I'll get to a little later.
These datatype codes are in some sense redundant, because
something like the TIFF_ImageWidth is always going to be an
integer (code 3 or 4), not a string (2) or a RATIONAL (5).
Most times you don't have a choice. And confusion can arise.
For example with TIFF_Copyright. Because a previous entry
might have TIFF_Artist specified, a naive person could
reasonably think the copyright was just a 4-digit year
(YYYY) for the copyright, which would be an integer, combining
it with the artist's name. As in
©2020 Picasso.
But this would be wrong, as the TIFF_Copyright entry is
expected to be a string (code 2), which could contain both
the year and the name of who the copyright belongs to. A
similar confusion can exist with the TIFF_Make and TIFF_Model
entries, since the (camera or scanner) model might just be a
number.
With the first two fields in an IFD directory entry covered thoroughly, the third field, an unsigned long (4-byte) integer is a count of the number of data. For something simple like TIFF_ImageWidth this will just be 1. For datatype 2 (ASCII strings) this will be the length of the string, the number of characters it contains. There are instances where this is a much larger number -- hence the use of long integers -- as we'll see soon enough.
The last of the four fields in an IFD directory entry is also an unsigned 4-byte (long) integer and contains either the data itself or a pointer (offset) to where the data can be found. For something simple like TIFF_ImageWidth this will just be the data, the width of the image, since this will almost certainly be able to fit within the range possible with long integers. But if the data requires more than 4 bytes, then this field will be an offset to the data, not the data itself. Reading programs need to be smart enough to take the data type and the number of data and figure out if the nominal data field is in fact the data or a pointer (offset) to the data.
For this reason there is almost always an IFD data section somewhere in the file, which in my code always follows the IFD. (I consider it part of the IFD.) Strings are a good example. A TIFF_Artist entry would contain a name, which is likely to take more than just four characters. It won't fit in the availabe 4-byte field the IFD entry itself provides, so instead the string is placed in the IFD data section and then pointed to in the IFD directory entry "data" field. If the TIFF_Artist was instead "None", then that string is short enough to fit in the data field. Any entry using RATIONALs will have to point to the IFD data section, since 8-bytes are needed to represent both the numerator and denominator.
The offset to the beginning of the IFD data section is easy to figure out if it immediately follows the IFD, since there's only an 8-byte "header" before the IFD, and then the IFD itself will take up 2 + (12*N) + 4 bytes, where N is the number of directory entries. This all simplifies to (12*N) + 14 bytes total down to the end of the IFD, after which I always start putting the data pointed to in the IFD. This is usually a number around 200 or a little higher. Of course you need to keep track of how many bytes total there are in the IFD data section so you know where the image data starts, since it follows immediately in the code I write.
Since I haven't mentioned it before, when TIFF was invented the various entries could be anywhere in the IFD, in any order. Later, it was decided and stipulated that they should be in order of increasing tag code, which is the way they're usually presented in an include file (and in the documentation). This makes some sense, and makes it easier to see if a particular entry is present or not, since you can quickly zero in on where it would be in the IFD rather than having to search through the whole thing.
With all that it's now necessary to delve into one of the TIFF format's unavoidable idiosyncracies, namely that the image data has to be broken up into "strips". This was done at the format's beginning because computer memory was at a premium then, and many, many times smaller than today, so by breaking the image up into strips -- originally 8 kilobytes or less in size -- one only needed a buffer that size in able to handle an image.
Today, of course, there are images with a width greater than 8k, which would make for only one row per strip, meaning several thousand strips. This, of course, would be ridiculous, but so far as I know there is now no recommended size in bytes for TIFF strips, so everyone is free to write out images any way they want, even if all as one strip.
In practice what I do is factor the height ("length" in TIFF terminology), the number of rows in the image, into its smallest prime numbers and then combine these in one of the several different possible ways to achieve something like 15-25 strips, each with the same number of rows. After writing code to write out probably several dozen images of different heights I've yet to encounter whose height that didn't factor into enough 2's, 3's, and/or 5's (and maybe a 7 or an 11) to make this possible. I will confess that at some point I made a little utility program for factoring any number into its constituent prime numbers, to speed this along. But for a long time I just did the factoring trial-and-error style on a hand calculator.
Some programs choose an arbitrary, fixed number of rows per strip to use for the bulk of the image, and then are left with some remainder number of rows at the bottom of the image for the last strip. This is permissible (I guess), but seems inelegant at best. I've seen small numbers, like 2 or 4, for the number of rows in the last strip in such circumstances. So the last strip is not necessarily the same number of rows as the value found in the IFD under TIFF_RowsPerStrip, depending on the program that created the file. According to the v.6 TIFF document TIFF_RowsPerStrip is a single number, not a table of multiple values, one for each strip (which would be an array of values in the program code). TIFF readers need to be to able to handle such quirks or exceptions.
Along these same lines, a TIFF reader has to set a maximum number of strips it can handle, since it needs to store arrays of values for the parameters to be discussed shortly, and in order to do so it has to set an array size (an upper limit) or be able to re-size a couple of existing arrays on the fly after finding out the image it's processing is composed of more strips than it's set up to handle. So far, I've got these arrays set to hold up to 100 values, and have not had any images that have wanted it to be higher.
There are several other IFD entries related to strips that you'll have to use. Perhaps the most important is TIFF_StripByteCounts, the number of bytes in a strip. One advantage of using a constant number of rows per strip with uncompressed data is that it'll be the same number for every strip. Which means in the IFD data section it will be a number simply repeated M times, where M is the number of strips.
The other important one is TIFF_StripOffsets, which is a series of numbers telling where each of the M strips begins in the file. Once the first value is calculated the subsequent ones each increase by TIFF_StripByteCounts from the previous one.
One peculiarity I've noticed about all this is that there is not an explicit IFD entry for the number of strips. Instead, the value of M appears in the number-of-data field (the 3rd field) of at least the TIFF_StripByteCounts and TIFF_StripOffsets entries.
Again, strips are not optional. Even if you write out the image as a single strip, the only way a reading program has to find the image data is via the TIFF_StripOffsets entry.
There are a couple of other things about TIFF worth mentioning. It is sometimes possible to do things simply that would otherwise be a little complicated. For example, let's say you have an 8-bit greyscale (B&W) image and want to invert it, turning a negative into a positive or vice versa. The brute force way to do this would be to read the image in, change every pixel value from K to 255-K, and then write it out. But with TIFF you can do this by changing just a single bit. This refers to the TIFF_PhotometricInterpretation IFD entry. Its data field contains a code specifying whether white is a data/pixel value of zero (code 0) or whether black is zero (code 1). So to invert an image all you have to do is flip the lowest order bit, and then the reading program is supposed to do all the hard work. (For 24-bit RGB images, code 2 is used, so these can't be inverted.)
There's another one of these which is similar. Let's say you want to rotate your image by 90°, either clockwise or counter-clockwise. The brute force method would involve making rows in the original into columns in the output. For a 180° rotation, rows would still be rows (and columns columns) but top would become bottom and the lefthand side would become the righthand side. Well, with the otherwise optional IFD entry TIFF_Orientation. Its data field contains a code between 1 and 8 specifying where the first pixel in the first row should go. The default is code 1, meaning the usual upper-left (UL) corner. The other odd numbered code values allow you to specify a different corner, amounting to a rotation, but you can also do a mirror-imaging at the same time, using the even numbered code values. Again, the reading program is supposed to do the hard work.
The final thing you should know about TIFF images is that the EXIF (Exchangeable image file format) metadata stored by almost all digital cameras since the late-1990s uses a format that is the same as TIFF. The TIFF_EXIFIFD IFD entry in a TIFF image file will contain a pointer or offset to the start of what might be called a sub-IFD containing the EXIF information in the same format as a TIFF file IFD, except of course it doesn't point to any image data.
It will start with a number of entries (2-bytes) followed by the 12-byte, four-field entries themselves. These have EXIF tag codes (so there's an EXIF_tags.inc include file you'll probably want), and will contain everything from the f-stop and exposure time the picture was taken with to the focal length of the lens used, the date/time, the camera make/model, the exposure "mode", whether the flash was fired or not, the firmware version of the software inside the camera, etc. Practically every setting the camera is capable of might be stored somewhere in the EXIF section, though for many if not most cameras it can be difficult to locate because the manufacturers don't make the exact way they do this public. They use something called the EXIF MakerNote tag to point to a block of information that is proprietary and might even be encrypted, so you have to use their software.
Some sleuths have disentangled the MakerNote info to some extent for some camera models. This is done by taking two pictures with one setting changed, and then comparing the two files byte by byte to find where they differ (except for the time of course). Needless to say, there's an EXIF_MakerNote.inc include file you'll probably want to help you find the known MakerNote IFD entry tag codes.
Along these lines, my camera has a temperature sensor in it, which I think might be near the actual CMOS sensor, but the value recorded in the EXIF section is not displayed by any image processing software I have, though it may show a bunch of the other EXIF information there. By poking around on the I-Net I was able to find someone who had figured out where this number was stored and, with my own code, I was able to access it.
[This was for Canon .CR2 "raw" files made with a particular model of camera. The 4th entry (of 31 entries) in the MakerNote sub-sub-IFD this camera makes is called, reasonably enough, ShotInfo. (The next model higher in this line of cameras has 35 entries.) The ShotInfo entry has a data field containing an offset pointing to a table or list of 34 short integers (35 in the next model up). The 13th is the temperature -- plus 128 degrees (°C of course), since it needs to be stored as an unsigned short integer. For this camera the offset to the temperature from the beginning of a file is constant, so all the pointers only have to be followed to find it once.]
This is a particularly valuable number to have for astronomical imaging because the background noise level in a picture taken with a long exposure depends primarily on the sensor temperature (and the exposure time of course). One simple thing I was able to do with this was figure out how long my camera needs to be outside in the cold on a winter night in the teens before it reaches the ambient temperature (i.e., before it stops cooling down any further). I can also see the camera warm up some as I start taking a series of pictures, since the energy to power the camera essentially gets dissipated inside the camera electronics.
[A little while after writing the above it occurred to me that not all readers would know that Canon thought so highly of the TIFF format when they created the .CR2 format that they made it a broken or bastardized version of TIFF. The difference starts just four bytes into the file, where instead of the 4-byte pointer 8 to the first of four IFDs, immediately following the "header", the number is 16. The 8 "hidden" bytes following the header consist of the two characters "CR", presumably short for Canon Raw, followed by two more characters for the major and minor "version" numbers (the former should be 2); the last 4 bytes are a pointer to the IFD for the full resolution, raw image data. But you don't want to go there, instead you'd want to go to the first IFD (at offset 16), which is where you'd find the TIFF_EXIFIFD entry to the EXIF IFD containing EXIF_MakerNote, where the temperature is.
The main way CR2 is a broken TIFF format is in the fact that the last three IFDs are not complete. They assume knowledge of information found in the first IFD. In a legit multi-image TIFF it's possible in principle to extract any or all of the images into single image files by referencing only the IFD pertaining to that image. The IFDs are independent of one another, except for the fact that one points to the next. Any data field containing a pointer would of course have to be re-calculated rather than copied, but this amounts to simply subtracting a constant number from all the pointers for that image. In a CR2 file the IFDs are not independent of one another.]
This page in progress...
©2022, Chris Wetherill. All rights reserved. Display of words or photos here does NOT constitute or imply permission to store, copy, republish, or redistribute my work in any manner for any purpose without expressed prior permission. -- except for the computer code, which is "open source" and carries the usual restrictions, namely that you can't use it for commercial purposes.